Security Extension Application Hardening¶
Now it is time to harden and protect your application against malicious attackers and from potential vulnerabilities. In order to add an additional layer of security in your application, remember the security that concepts we introduced in previous sections, such as setting up a container with read-only file system, initializing a container, adding/dropping Linux capabilities, and using a non-root user.
Mount read-only volume on MQTT broker¶
Using a read-only volume on MQTT broker protects you against an attacker modifying your MQTT credentials, even if the attacker has privileged user rights.
To show you how to mount a read-only volume on the container, we will use the example of MQTT by adding the following line in the MQTT docker-compose file:
# Open the docker-compose.yml from /src/solution/HandsOn_2/mqtt_broker_mosquitto
version: '2.4' # docker-compose version is set to 2.4
services:
mqtt-broker:
image: eclipse-mosquitto:$MQTT_VERSION # define image to pull from docker hub if not already available on your machine
container_name: ie-databus # Name of MQTT broker container
restart: unless-stopped # always restarts (see overview on page 12 of Industrial Edge Developer Guide)
read_only: true # Read-only file system
logging: # allow logging
options: # we use best practice here as limiting file size and rolling mechanism
max-size: "10m" # File size is 10MB
max-file: "2" # only 2 files created before rolling mechanism applies
volumes: # mount volume from host
- mosquitto:/mosquitto:ro # set to read-only volume
ports: # expose ports and publish
- "33083:1883" # map containers default MQTT port (1883) to host's port 33083
networks: # define networks connected to container 'mqtt-broker'
proxy-redirect: # Name of the network
###### NETWORK CONFIG ######
networks: # Network interface configuration
proxy-redirect: # Reference 'proxy-redirect' as predefined network
name: proxy-redirect
driver: bridge
###### VOLUMES ######
volumes: # Volumes for containers
mosquitto:
Now you can build the docker-compose file again and run the MQTT container:
# Start the docker-compose file
docker-compose up --build
# Open another terminal
# Check if the mounted volume is read-only
docker ps
>>> CONTAINER ID IMAGE COMMAND CREATED ...
>>> aab72de9946f eclipse-mosquitto:1.6.14 "/docker-entrypoint.…" 2 minutes ago ...
docker inspect aab72de9946f
>>> ...
>>> {
>>> "Type": "volume",
>>> "Name": "mqtt_broker_mosquitto_mosquitto",
>>> "Source": "/var/lib/docker/volumes/mqtt_broker_mosquitto_mosquitto/_data",
>>> "Destination": "/mosquitto",
>>> "Driver": "local",
>>> "Mode": "z",
>>> "RW": false,
>>> "Propagation": ""
>>> }
As it shows in the terminal, the read-write operation ("RW") is set to false, which means the mounted volume is read-only.
Setup initial container in Influxdb¶
In the earlier docker section, we discussed how to initialize a container with a relative easy example. Using init containers, you can load secrets from various sources and initialize your application. You can also create a bad practice warning when a developer tries to hardcode the username and password in plain files. For the sake of simplicity, we do not discuss these details in the developer guide.
In the Hands On section, we will use the concept to initialize the influxdb container environment.
# Open the docker-compose.yml from /src/solution/HandsOn_2/my_edge_app
...
##### INFLUXDB ######
init-db:
build:
context: ./influxdb
image: init-db:v0.0.1 # Docker Init Container to setup the Main InfluxDB v2.2 Container
depends_on:
influxdb:
condition: service_healthy
networks: # define networks connected to container 'influxdb'
proxy-redirect: # Name of the network
influxdb:
image: influxdb:2.2-alpine # Define image to pull from docker hub if not already available on your machine
container_name: influxdb # Name of the influx-db container
restart: unless-stopped # always restarts (see overview on page 12 of Industrial Edge Developer Guide)
mem_limit: 350m
logging: # allow logging
options: # we use best practice here as limiting file size and rolling mechanism
max-size: "10m" # File size is 10MB
max-file: "2" # only 2 files created before rolling mechanism applies
driver: json-file
ports: # expose ports and publish
- "8086:8086" # map containers port 8086 to host's port 8086
volumes: # mount volume from host
- db-backup:/var/lib/influxdb # mount named volume 'db-backup' to host's path to /var/lib/influxdb
healthcheck:
test: "exit 0"
networks: # define networks connected to container 'influxdb'
proxy-redirect: # Name of the network
...
The init-db container starts after the influxdb container and exits after executing the init_influxdb.sh
bash script. The bash script sets up the user, credentials, org, retention period, database, and token automatically. The influxdb container service checks if the container is in a healthy status, and it will exit as 0. If so, the init-db container service starts after 30 seconds, because the check "exit 0" is triggered after 30 seconds as it is the default.
Info
For more information see Healthcheck section of the docker documentation.
# Start docker-compose file under /src/solution/HandsOn_2/my_edge_app
docker-compose up
You can access the influxdb from https://localhost:38086 with the following credentials:
user: edge
password: edgeadmin
You can now verify that the account settings were set up properly.
Select and drop docker capabilities¶
In the previous section we discussed the docker capabilities and their implications for container security at runtime. In this section, we will be using the examples of nginx and grafana containers to show how to select and drop unnecessary docker capabilities, in order to achieve fine-grained control of security capabilities.
Use of Tracee for Nginx containers¶
Use the Tracee tool to list the system calls being generated by a container. The tool can also be used to trace out cap_capable events, showing the capabilities that a newly created container requests from the kernel.
# Open a new terminal
# Go into the Hands-on-App directory src/solution/HandsOn_2/tracee
cd src/solution/HandsOn_2/tracee
# Start docker-compose file
docker-compose up
# Output
>>> INFO: probing tracee-ebpf capabilities...
>>> TIME CONTAINER_ID UID COMM PID/host TID/host RET EVENT ARGS
As an example, we will show the first few events traced from a nginx container.
# Open another terminal
# Go into the src/solution/HandsOn_2/example
cd ../example
# Start docker-compose file
docker-compose -f docker-compose.web.yml up --build
# Open browser on
http://localhost:8080
# After it runs successfully, you can close the container
docker-compose down
You can see the following outputs from the Tracee terminal. You can also find the logs in /var/lib/docker/container/container_id
or you can execute command docker logs tracee
in terminal to get the logs.
INFO: probing tracee-ebpf capabilities...
TIME CONTAINER_ID UID COMM EVENT ARGS
13:59:03:600324 a71789d1a0e1 0 nginx cap_capable cap: CAP_DAC_READ_SEARCH
13:59:03:603116 a71789d1a0e1 0 nginx cap_capable cap: CAP_CHOWN
13:59:03:605167 a71789d1a0e1 0 nginx cap_capable cap: CAP_DAC_OVERRIDE
13:59:03:609840 a71789d1a0e1 0 nginx cap_capable cap: CAP_SETGID
13:59:03:613501 a71789d1a0e1 0 nginx cap_capable cap: CAP_SETUID
Once you know which capabilities your container needs, you can follow the principle of least privilege and specify at runtime the precise set that should be granted. The recommended approach is to drop all capabilities and then add back the necessary ones as follows:
# Open and edit the docker-compose.web.yml
version: '2.4'
services:
web:
image: nginx
volumes:
- ./templates:/etc/nginx/templates
ports:
- "8080:80"
environment:
- NGINX_HOST=foobar.com
- NGINX_PORT=80
cap_drop: # Drop all linux capabilities
- ALL
cap_add: # Select only the necessary capabilities from tracee tool
- DAC_READ_SEARCH
- DAC_OVERRIDE
- CHOWN
- SETGID
- SETUID
Now run the docker-compose file again
# Under the same terminal
# Start docker-compose file
docker-compose -f docker-compose.web.yml up --build
# Open browser on
http://localhost:8080
By selecting the necessary capabilities, the nginx container runs successfully without any problems, even if we drop all other docker capabilities.
Rinse-and-repeat strategy¶
Based on the selected capabilities, you can also test and drop other added capabilities one by one until the container throws permission errors. In the nginx case you do not need CAP_DAC_OVERRIDE
and CAP_DAC_READ_SEARCH
because the nginx container starts as root, which has CAP_DAC_OVERRIDE
capability by default. However it is not needed during runtime.
In conclusion, you can edit the docker-compose file as follows:
# Open and edit the docker-compose.web.yml
version: '2.4'
services:
web:
image: nginx
volumes:
- ./templates:/etc/nginx/templates
ports:
- "8080:80"
environment:
- NGINX_HOST=foobar.com
- NGINX_PORT=80
cap_drop: # Drop all Linux capabilities
- ALL
cap_add: # Select only the necessary capabilities from Tracee tool
- CHOWN
- SETGID
- SETUID
Implement a non-root user¶
In the previous section, we discussed how a non-root user works in docker. As an example, we will use the data-analytics container to initiate the container with a non-root user.
# Open the Dockerfile from /src/solution/HandsOn_2/my_edge_app/data-analytics
ARG BASE_IMAGE
FROM ${BASE_IMAGE}
# Add non-root user
RUN adduser -S nonroot
# install all requirements from requirements.txt
COPY requirements.txt /
RUN pip install -r /requirements.txt; rm -f /requirements.txt
# Set the working directory to /app
WORKDIR /app
# Copy the current dir into the container at /app
COPY ./program/* /app/
# Use the
USER nonroot
# Run app.py when the container launches
CMD ["python", "-u", "-m", "app"]
Now you can build the docker-compose file again and run it with Node-RED and MQTT containers:
# In the /src/solution/HandsOn_2/mqtt_broker_mosquitto directory
docker-compose up
# In the /src/solution/HandsOn_2/my_edge_app directory
docker-compose up -build
# In the /src/solution/HandsOn_2/node_red directory
docker-compose up
# Check which user the data-analytics container is running
docker ps
>>> CONTAINER ID IMAGE COMMAND ...
>>> abcbf7de7e6f data-analytics:v0.0.1 "python -u -m app" ...
docker exec -it abcbf7de7e6f /bin/sh
/app $ whoami
>>> nonroot
# Open another terminal
docker inspect --format '{{.State.Pid}}' abcbf7de7e6f
>>> 24957
# There is no capability available in the container
getpcaps 24957
>>> 24957:=
You can see from the terminal that the data-analytics container is running as a non-root user and has no Linux capability, while the application runs without problems.
This was a brief overview of the main points of the Python app which is contained in 'my_edge_app'.
Overview of the Docker-Compose file after hardening¶
The docker-compose.yml is used to build the containers. An overview of docker-compose.yml after security hardening is shown below.